@xano/cli 1.2.0 → 1.3.1-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 +381 -4
- package/dist/base-command.d.ts +54 -36
- package/dist/base-command.js +246 -169
- package/dist/commands/auth/index.d.ts +1 -1
- package/dist/commands/auth/index.js +7 -7
- package/dist/commands/branch/create/index.d.ts +1 -1
- package/dist/commands/branch/create/index.js +1 -1
- package/dist/commands/branch/set_live/index.d.ts +22 -0
- package/dist/commands/branch/set_live/index.js +60 -1
- package/dist/commands/ephemeral/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/create/index.js +103 -0
- package/dist/commands/ephemeral/delete/index.d.ts +18 -0
- package/dist/commands/ephemeral/delete/index.js +99 -0
- package/dist/commands/ephemeral/edit/index.d.ts +18 -0
- package/dist/commands/ephemeral/edit/index.js +109 -0
- package/dist/commands/ephemeral/get/index.d.ts +21 -0
- package/dist/commands/ephemeral/get/index.js +125 -0
- package/dist/commands/ephemeral/impersonate/index.d.ts +20 -0
- package/dist/commands/ephemeral/impersonate/index.js +120 -0
- package/dist/commands/ephemeral/list/index.d.ts +19 -0
- package/dist/commands/ephemeral/list/index.js +123 -0
- package/dist/commands/ephemeral/pull/index.d.ts +19 -0
- package/dist/commands/ephemeral/pull/index.js +163 -0
- package/dist/commands/ephemeral/push/index.d.ts +35 -0
- package/dist/commands/ephemeral/push/index.js +228 -0
- package/dist/commands/ephemeral/static_host/build/delete/index.d.ts +20 -0
- package/dist/commands/ephemeral/static_host/build/delete/index.js +121 -0
- package/dist/commands/ephemeral/static_host/build/get/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/get/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/list/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/build/list/index.js +137 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.d.ts +53 -0
- package/dist/commands/ephemeral/static_host/build/pull/index.js +308 -0
- package/dist/commands/ephemeral/static_host/build/push/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/build/push/index.js +250 -0
- package/dist/commands/ephemeral/static_host/create/index.d.ts +18 -0
- package/dist/commands/ephemeral/static_host/create/index.js +92 -0
- package/dist/commands/ephemeral/static_host/deploy/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/deploy/index.js +112 -0
- package/dist/commands/ephemeral/static_host/edit/index.d.ts +24 -0
- package/dist/commands/ephemeral/static_host/edit/index.js +158 -0
- package/dist/commands/ephemeral/static_host/get/index.d.ts +19 -0
- package/dist/commands/ephemeral/static_host/get/index.js +101 -0
- package/dist/commands/ephemeral/static_host/list/index.d.ts +17 -0
- package/dist/commands/ephemeral/static_host/list/index.js +132 -0
- package/dist/commands/flatten/index.d.ts +34 -0
- package/dist/commands/flatten/index.js +140 -0
- package/dist/commands/function/create/index.d.ts +15 -0
- package/dist/commands/function/create/index.js +41 -1
- package/dist/commands/function/edit/index.d.ts +15 -0
- package/dist/commands/function/edit/index.js +66 -32
- package/dist/commands/function/get/index.js +2 -3
- package/dist/commands/function/run/index.d.ts +1 -0
- package/dist/commands/function/run/index.js +19 -6
- package/dist/commands/policy/catalogue/index.d.ts +15 -0
- package/dist/commands/policy/catalogue/index.js +29 -0
- package/dist/commands/policy/create/index.d.ts +18 -0
- package/dist/commands/policy/create/index.js +39 -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/profile/create/index.d.ts +1 -1
- package/dist/commands/profile/create/index.js +7 -7
- package/dist/commands/profile/me/index.js +1 -1
- package/dist/commands/profile/use/index.js +0 -4
- package/dist/commands/profile/wizard/index.d.ts +1 -1
- package/dist/commands/profile/wizard/index.js +12 -10
- package/dist/commands/profile/workspace/set/index.js +1 -1
- package/dist/commands/release/create/index.js +7 -0
- package/dist/commands/release/deploy/index.d.ts +28 -0
- package/dist/commands/release/deploy/index.js +90 -6
- package/dist/commands/release/edit/index.js +2 -2
- package/dist/commands/release/export/index.js +1 -2
- package/dist/commands/release/import/index.js +6 -2
- package/dist/commands/release/list/index.d.ts +7 -0
- package/dist/commands/release/list/index.js +54 -10
- package/dist/commands/release/pull/index.d.ts +0 -6
- package/dist/commands/release/pull/index.js +13 -99
- package/dist/commands/release/push/index.js +21 -3
- package/dist/commands/sandbox/env/get_all/index.js +1 -1
- package/dist/commands/sandbox/env/set_all/index.js +1 -1
- package/dist/commands/sandbox/get/index.js +1 -1
- package/dist/commands/sandbox/license/get/index.js +1 -1
- package/dist/commands/sandbox/license/set/index.js +1 -1
- package/dist/commands/sandbox/pull/index.d.ts +0 -1
- package/dist/commands/sandbox/pull/index.js +13 -81
- package/dist/commands/sandbox/push/index.d.ts +1 -1
- package/dist/commands/sandbox/push/index.js +11 -6
- package/dist/commands/skills/pull/index.d.ts +30 -0
- package/dist/commands/skills/pull/index.js +118 -0
- package/dist/commands/static_host/build/create/index.d.ts +1 -1
- package/dist/commands/static_host/build/create/index.js +2 -2
- package/dist/commands/static_host/build/pull/index.js +1 -1
- package/dist/commands/static_host/build/push/index.js +1 -1
- package/dist/commands/static_host/edit/index.js +1 -1
- package/dist/commands/tenant/backup/export/index.js +1 -2
- package/dist/commands/tenant/backup/import/index.js +1 -1
- package/dist/commands/tenant/cluster/create/index.js +1 -1
- package/dist/commands/tenant/cluster/license/get/index.js +1 -1
- package/dist/commands/tenant/cluster/license/set/index.js +1 -1
- package/dist/commands/tenant/create/index.d.ts +4 -1
- package/dist/commands/tenant/create/index.js +26 -5
- package/dist/commands/tenant/deploy_platform/index.js +1 -1
- package/dist/commands/tenant/deploy_release/index.d.ts +40 -0
- package/dist/commands/tenant/deploy_release/index.js +186 -4
- package/dist/commands/tenant/edit/index.d.ts +2 -0
- package/dist/commands/tenant/edit/index.js +46 -11
- package/dist/commands/tenant/env/get_all/index.js +1 -1
- package/dist/commands/tenant/env/set_all/index.js +1 -1
- package/dist/commands/tenant/impersonate/index.d.ts +1 -1
- package/dist/commands/tenant/impersonate/index.js +14 -14
- package/dist/commands/tenant/license/get/index.js +1 -1
- package/dist/commands/tenant/license/set/index.js +1 -1
- package/dist/commands/tenant/pull/index.d.ts +0 -6
- package/dist/commands/tenant/pull/index.js +14 -100
- 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 +116 -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/git/pull/index.d.ts +4 -4
- package/dist/commands/workspace/git/pull/index.js +158 -97
- package/dist/commands/workspace/pull/index.d.ts +9 -5
- package/dist/commands/workspace/pull/index.js +83 -106
- package/dist/commands/workspace/push/index.d.ts +19 -0
- package/dist/commands/workspace/push/index.js +124 -5
- package/dist/help.js +4 -2
- package/dist/policy-command.d.ts +90 -0
- package/dist/policy-command.js +94 -0
- package/dist/update-check.d.ts +1 -1
- package/dist/update-check.js +1 -1
- package/dist/utils/document-parser.d.ts +126 -0
- package/dist/utils/document-parser.js +332 -4
- package/dist/utils/flatten.d.ts +36 -0
- package/dist/utils/flatten.js +68 -0
- package/dist/utils/knowledge-sync.d.ts +1 -0
- package/dist/utils/knowledge-sync.js +11 -6
- package/dist/utils/microservice-wait.d.ts +53 -0
- package/dist/utils/microservice-wait.js +84 -0
- package/dist/utils/multidoc-push.d.ts +143 -2
- package/dist/utils/multidoc-push.js +479 -40
- package/dist/utils/paging.d.ts +2 -1
- package/dist/utils/paging.js +0 -1
- package/dist/utils/policy/catalogue.d.ts +7 -0
- package/dist/utils/policy/catalogue.js +72 -0
- package/dist/utils/policy/errors.d.ts +12 -0
- package/dist/utils/policy/errors.js +61 -0
- package/dist/utils/policy/feedback.d.ts +68 -0
- package/dist/utils/policy/feedback.js +165 -0
- package/dist/utils/policy/findings.d.ts +23 -0
- package/dist/utils/policy/findings.js +65 -0
- package/dist/utils/policy/gate.d.ts +161 -0
- package/dist/utils/policy/gate.js +201 -0
- package/dist/utils/policy/goal.d.ts +46 -0
- package/dist/utils/policy/goal.js +95 -0
- package/dist/utils/policy/permission.d.ts +23 -0
- package/dist/utils/policy/permission.js +84 -0
- package/dist/utils/policy/push-gate.d.ts +5 -0
- package/dist/utils/policy/push-gate.js +29 -0
- package/dist/utils/policy/release.d.ts +85 -0
- package/dist/utils/policy/release.js +115 -0
- package/dist/utils/policy/request.d.ts +55 -0
- package/dist/utils/policy/request.js +115 -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 +47 -0
- package/dist/utils/policy/status.js +117 -0
- package/dist/utils/policy/types.d.ts +185 -0
- package/dist/utils/policy/types.js +1 -0
- package/dist/utils/reference-checker.js +14 -3
- package/oclif.manifest.json +8538 -3445
- package/package.json +16 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { releasePolicyRunLines } from '../../../utils/policy/release.js';
|
|
3
4
|
export default class ReleaseCreate extends BaseCommand {
|
|
4
5
|
static args = {
|
|
5
6
|
name: Args.string({
|
|
@@ -11,6 +12,10 @@ export default class ReleaseCreate extends BaseCommand {
|
|
|
11
12
|
static examples = [
|
|
12
13
|
`$ xano release create "v1.0" --branch main
|
|
13
14
|
Created release: v1.0 - ID: 10
|
|
15
|
+
Branch: main
|
|
16
|
+
Policy check: fail (run 1712), 2 blocking, 1 advisory findings
|
|
17
|
+
Findings: xano policy runs --release v1.0
|
|
18
|
+
Its blocking findings block a deploy to a standard or run tenant: xano tenant deploy_release <tenant> --release v1.0 --check
|
|
14
19
|
`,
|
|
15
20
|
`$ xano release create "v1.1-hotfix" --branch main --hotfix --description "Critical fix" -o json`,
|
|
16
21
|
];
|
|
@@ -90,6 +95,8 @@ Created release: v1.0 - ID: 10
|
|
|
90
95
|
this.log(` Branch: ${release.branch}`);
|
|
91
96
|
if (release.hotfix)
|
|
92
97
|
this.log(` Hotfix: true`);
|
|
98
|
+
for (const line of releasePolicyRunLines(release.policy_run, release.name ?? args.name))
|
|
99
|
+
this.log(line);
|
|
93
100
|
}
|
|
94
101
|
}
|
|
95
102
|
catch (error) {
|
|
@@ -9,12 +9,40 @@ export default class ReleaseDeploy extends BaseCommand {
|
|
|
9
9
|
branch: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
10
10
|
force: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
11
11
|
output: import("@oclif/core/interfaces").OptionFlag<string, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
|
+
'policy-override': import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
13
|
set_live: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
13
14
|
workspace: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
14
15
|
config: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
15
16
|
profile: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
17
|
verbose: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
17
18
|
};
|
|
19
|
+
/** Set once the platform refuses set live for a weakened mandatory policy: already explained, it exits 1 as it is. */
|
|
20
|
+
private refusedByPermission;
|
|
21
|
+
/** Set once the set-live policy gate refuses, the one failure that exits 2. */
|
|
22
|
+
private refusedByPolicyGate;
|
|
23
|
+
/** Exit 2 is the set-live gate's refusal; every other failure, flag errors included, exits 1. */
|
|
24
|
+
protected catch(error: Error & {
|
|
25
|
+
oclif?: {
|
|
26
|
+
exit?: number;
|
|
27
|
+
};
|
|
28
|
+
}): Promise<void>;
|
|
18
29
|
run(): Promise<void>;
|
|
19
30
|
private confirm;
|
|
31
|
+
/** The set-live verdict a successful deploy answered with --set_live (`pass`, `overridden`, ...). */
|
|
32
|
+
private logPolicyOutcome;
|
|
33
|
+
/** Stops the command when set live was refused after the branch was created: by the gate (exit 2) or for a weakened mandatory policy (exit 1). */
|
|
34
|
+
private refuseIfSetLiveRefused;
|
|
35
|
+
/**
|
|
36
|
+
* The set-live gate refused after the branch was created: the verdict, the refusal, and how to
|
|
37
|
+
* proceed, exiting 2. Under `-o json` the refusal is `{branch_created, set_live, branch, message,
|
|
38
|
+
* policy_gate}`.
|
|
39
|
+
*/
|
|
40
|
+
private refuseSetLive;
|
|
41
|
+
/**
|
|
42
|
+
* Set live was refused after the branch was created because the release weakens (or lacks) a
|
|
43
|
+
* mandatory policy of the live branch, and the credential lacks `workspace:policy` update. That is a
|
|
44
|
+
* permission refusal, not a blocking finding: it exits 1 (under `-o json` as `{error}`) with the
|
|
45
|
+
* platform's sentence, which names the branch, and how to go on.
|
|
46
|
+
*/
|
|
47
|
+
private refuseWeakening;
|
|
20
48
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import BaseCommand from '../../../base-command.js';
|
|
3
|
+
import { BLANK_POLICY_OVERRIDE, blankPolicyOverride, gateOverrideHint, gateRefusal, liveGateLines, quoted, weakeningRefusal, } from '../../../utils/policy/gate.js';
|
|
4
|
+
import { policyWeakeningGuidance } from '../../../utils/policy/permission.js';
|
|
3
5
|
export default class ReleaseDeploy extends BaseCommand {
|
|
4
6
|
static args = {
|
|
5
7
|
release_name: Args.string({
|
|
@@ -7,7 +9,7 @@ export default class ReleaseDeploy extends BaseCommand {
|
|
|
7
9
|
required: true,
|
|
8
10
|
}),
|
|
9
11
|
};
|
|
10
|
-
static description =
|
|
12
|
+
static description = "[IMPORTANT] ALWAYS confirm with the user before deploying a release. Deploys a release to its workspace as a new branch. With --set_live the branch is then set live through the set-live policy gate: when blocking findings refuse it, the branch stays, set live is refused, and the command exits 2 unless --policy-override gives a reason. A release that weakens a mandatory policy of the live branch needs workspace:policy update to be set live; without it the branch stays and the command exits 1.";
|
|
11
13
|
static examples = [
|
|
12
14
|
`$ xano release deploy "v1.0"
|
|
13
15
|
Are you sure you want to deploy release "v1.0"? (y/N) y
|
|
@@ -18,6 +20,7 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
18
20
|
`,
|
|
19
21
|
`$ xano release deploy "v1.0" --branch "restore-v1" --no-set_live`,
|
|
20
22
|
`$ xano release deploy "v1.0" -w 40 -o json --force`,
|
|
23
|
+
`$ xano release deploy "v1.0" --branch "rollback-v1" --set_live --policy-override "Rollback approved; finding tracked in JIRA-12"`,
|
|
21
24
|
];
|
|
22
25
|
static flags = {
|
|
23
26
|
...BaseCommand.baseFlags,
|
|
@@ -39,6 +42,7 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
39
42
|
options: ['summary', 'json'],
|
|
40
43
|
required: false,
|
|
41
44
|
}),
|
|
45
|
+
'policy-override': Flags.string({ description: 'With --set_live, set the branch live past a blocking set-live policy gate with an audited reason (requires workspace:policy update)' }),
|
|
42
46
|
set_live: Flags.boolean({
|
|
43
47
|
default: false,
|
|
44
48
|
description: '[CRITICAL] STOP and confirm with the user before setting the deployed branch as live.',
|
|
@@ -50,8 +54,20 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
50
54
|
required: false,
|
|
51
55
|
}),
|
|
52
56
|
};
|
|
57
|
+
/** Set once the platform refuses set live for a weakened mandatory policy: already explained, it exits 1 as it is. */
|
|
58
|
+
refusedByPermission = false;
|
|
59
|
+
/** Set once the set-live policy gate refuses, the one failure that exits 2. */
|
|
60
|
+
refusedByPolicyGate = false;
|
|
61
|
+
/** Exit 2 is the set-live gate's refusal; every other failure, flag errors included, exits 1. */
|
|
62
|
+
async catch(error) {
|
|
63
|
+
if (this.refusedByPolicyGate)
|
|
64
|
+
return super.catch(error);
|
|
65
|
+
return this.catchAsOperational(error);
|
|
66
|
+
}
|
|
53
67
|
async run() {
|
|
54
68
|
const { args, flags } = await this.parse(ReleaseDeploy);
|
|
69
|
+
if (blankPolicyOverride(flags['policy-override']))
|
|
70
|
+
this.error(BLANK_POLICY_OVERRIDE);
|
|
55
71
|
const { profile } = this.resolveProfile(flags);
|
|
56
72
|
const workspaceId = flags.workspace || profile.workspace;
|
|
57
73
|
if (!workspaceId) {
|
|
@@ -59,11 +75,10 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
59
75
|
}
|
|
60
76
|
const releaseName = encodeURIComponent(args.release_name);
|
|
61
77
|
const apiUrl = `${profile.instance_origin}/api:meta/workspace/${workspaceId}/release/${releaseName}/deploy`;
|
|
62
|
-
const body =
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
body.branch = flags.branch;
|
|
78
|
+
const body = deployBody(flags);
|
|
79
|
+
if (body.override_reason && !flags.set_live) {
|
|
80
|
+
this.error('--policy-override only applies with --set_live: a branch that is not set live is not gated.');
|
|
81
|
+
}
|
|
67
82
|
if (!flags.force) {
|
|
68
83
|
const confirmed = await this.confirm(`Are you sure you want to deploy release "${args.release_name}" to workspace ${workspaceId}?`);
|
|
69
84
|
if (!confirmed) {
|
|
@@ -84,6 +99,7 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
84
99
|
method: 'POST',
|
|
85
100
|
}, flags.verbose, profile.access_token);
|
|
86
101
|
if (!response.ok) {
|
|
102
|
+
await this.refuseIfSetLiveRefused(response, flags, args.release_name);
|
|
87
103
|
const errorText = await response.text();
|
|
88
104
|
this.error(`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
89
105
|
}
|
|
@@ -98,10 +114,17 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
98
114
|
this.log(`Deployed release "${release.name}" to workspace ${workspaceId} (branch: ${branchLabel}${liveStatus})`);
|
|
99
115
|
if (release.description)
|
|
100
116
|
this.log(` Description: ${release.description}`);
|
|
117
|
+
this.logPolicyOutcome(release);
|
|
101
118
|
this.log(` Time: ${elapsed}s`);
|
|
102
119
|
}
|
|
120
|
+
// A policy of the release the new branch could not take is never dropped silently.
|
|
121
|
+
const skipped = release.policies_skipped?.message;
|
|
122
|
+
if (skipped)
|
|
123
|
+
this.warn(skipped);
|
|
103
124
|
}
|
|
104
125
|
catch (error) {
|
|
126
|
+
if (this.refusedByPolicyGate || this.refusedByPermission)
|
|
127
|
+
throw error;
|
|
105
128
|
if (error instanceof Error) {
|
|
106
129
|
this.error(`Failed to deploy release: ${error.message}`);
|
|
107
130
|
}
|
|
@@ -123,4 +146,65 @@ Deployed release "v1.0" to workspace 40 (branch: v1.0)
|
|
|
123
146
|
});
|
|
124
147
|
});
|
|
125
148
|
}
|
|
149
|
+
/** The set-live verdict a successful deploy answered with --set_live (`pass`, `overridden`, ...). */
|
|
150
|
+
logPolicyOutcome(release) {
|
|
151
|
+
const status = release.policy_gate?.status;
|
|
152
|
+
if (status)
|
|
153
|
+
this.log(` Policy gate: ${status}`);
|
|
154
|
+
}
|
|
155
|
+
/** Stops the command when set live was refused after the branch was created: by the gate (exit 2) or for a weakened mandatory policy (exit 1). */
|
|
156
|
+
async refuseIfSetLiveRefused(response, flags, releaseName) {
|
|
157
|
+
const refused = await gateRefusal(response);
|
|
158
|
+
if (refused)
|
|
159
|
+
this.refuseSetLive(refused, flags, releaseName);
|
|
160
|
+
const weakening = await weakeningRefusal(response);
|
|
161
|
+
if (weakening)
|
|
162
|
+
this.refuseWeakening(weakening, flags);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The set-live gate refused after the branch was created: the verdict, the refusal, and how to
|
|
166
|
+
* proceed, exiting 2. Under `-o json` the refusal is `{branch_created, set_live, branch, message,
|
|
167
|
+
* policy_gate}`.
|
|
168
|
+
*/
|
|
169
|
+
refuseSetLive(refused, flags, releaseName) {
|
|
170
|
+
const answer = refused.answer;
|
|
171
|
+
const label = answer.branch?.label || flags.branch || 'the release branch';
|
|
172
|
+
if (flags.output === 'json') {
|
|
173
|
+
this.log(JSON.stringify({ branch: answer.branch ?? null, branch_created: true, message: refused.message, policy_gate: answer, set_live: false }, null, 2));
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
for (const line of liveGateLines(answer, 'release'))
|
|
177
|
+
this.log(line);
|
|
178
|
+
}
|
|
179
|
+
const workspace = flags.workspace ? ` -w ${quoted(flags.workspace)}` : '';
|
|
180
|
+
const rerun = `xano branch delete ${quoted(label)}${workspace} && xano release deploy ${quoted(releaseName)}${workspace}${flags.branch ? ` --branch ${quoted(flags.branch)}` : ''} --set_live --policy-override "<why>"`;
|
|
181
|
+
const hint = gateOverrideHint(answer, rerun);
|
|
182
|
+
this.refusedByPolicyGate = true;
|
|
183
|
+
this.error(`The branch was created; set live was refused. ${refused.message}${hint}${answer.can_override && !answer.override_denied ? `\nOr set "${label}" live from Studio's Branches panel, which asks for the reason.` : ''}`, { exit: 2 });
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Set live was refused after the branch was created because the release weakens (or lacks) a
|
|
187
|
+
* mandatory policy of the live branch, and the credential lacks `workspace:policy` update. That is a
|
|
188
|
+
* permission refusal, not a blocking finding: it exits 1 (under `-o json` as `{error}`) with the
|
|
189
|
+
* platform's sentence, which names the branch, and how to go on.
|
|
190
|
+
*/
|
|
191
|
+
refuseWeakening(refused, flags) {
|
|
192
|
+
const label = refused.payload.branch?.label || flags.branch;
|
|
193
|
+
const workspace = flags.workspace ? ` -w ${quoted(flags.workspace)}` : '';
|
|
194
|
+
const ways = label
|
|
195
|
+
? [`The branch stays. To remove it: xano branch delete ${quoted(label)}${workspace}`, `Someone with that permission can set it live: xano branch set_live ${quoted(label)}${workspace}`]
|
|
196
|
+
: [];
|
|
197
|
+
this.refusedByPermission = true;
|
|
198
|
+
this.error(`${refused.message}${policyWeakeningGuidance(refused.payload, ways, true)}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** The deploy request's body: the new branch's label, set_live, and the override reason when one is given. */
|
|
202
|
+
function deployBody(flags) {
|
|
203
|
+
const body = { set_live: flags.set_live };
|
|
204
|
+
if (flags.branch)
|
|
205
|
+
body.branch = flags.branch;
|
|
206
|
+
const overrideReason = flags['policy-override']?.trim();
|
|
207
|
+
if (overrideReason)
|
|
208
|
+
body.override_reason = overrideReason;
|
|
209
|
+
return body;
|
|
126
210
|
}
|
|
@@ -69,8 +69,8 @@ Updated release: v1.0-final - ID: 10
|
|
|
69
69
|
const current = await getResponse.json();
|
|
70
70
|
// Merge in user-provided values
|
|
71
71
|
const body = {
|
|
72
|
-
description: flags.description
|
|
73
|
-
name: flags.name
|
|
72
|
+
description: flags.description === undefined ? (current.description ?? '') : flags.description,
|
|
73
|
+
name: flags.name === undefined ? current.name : flags.name,
|
|
74
74
|
};
|
|
75
75
|
// Update release
|
|
76
76
|
const putResponse = await this.verboseFetch(baseUrl, {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
-
import
|
|
3
|
+
import path from 'node:path';
|
|
4
4
|
import BaseCommand, { buildUserAgent } from '../../../base-command.js';
|
|
5
5
|
export default class ReleaseExport extends BaseCommand {
|
|
6
6
|
static args = {
|
|
@@ -79,7 +79,6 @@ Downloaded release 'v1.0' to ./release-v1.0.tar.gz
|
|
|
79
79
|
const fileStream = fs.createWriteStream(resolvedPath);
|
|
80
80
|
const reader = downloadResponse.body.getReader();
|
|
81
81
|
let totalBytes = 0;
|
|
82
|
-
// eslint-disable-next-line no-constant-condition
|
|
83
82
|
while (true) {
|
|
84
83
|
// eslint-disable-next-line no-await-in-loop
|
|
85
84
|
const { done, value } = await reader.read();
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
-
import
|
|
3
|
+
import path from 'node:path';
|
|
4
4
|
import BaseCommand from '../../../base-command.js';
|
|
5
|
+
import { releaseImportRefusal, releasePolicyRunLines } from '../../../utils/policy/release.js';
|
|
5
6
|
export default class ReleaseImport extends BaseCommand {
|
|
6
7
|
static args = {};
|
|
7
8
|
static description = '[IMPORTANT] ALWAYS confirm with the user before importing a release. Imports a release file into a workspace.';
|
|
@@ -58,7 +59,8 @@ Imported release as #15
|
|
|
58
59
|
}, flags.verbose, profile.access_token);
|
|
59
60
|
if (!response.ok) {
|
|
60
61
|
const errorText = await response.text();
|
|
61
|
-
this.error(
|
|
62
|
+
this.error(releaseImportRefusal(errorText, response.status, apiUrl) ??
|
|
63
|
+
`API request failed with status ${response.status}: ${response.statusText}\n${errorText}`);
|
|
62
64
|
}
|
|
63
65
|
const result = await response.json();
|
|
64
66
|
if (flags.output === 'json') {
|
|
@@ -67,6 +69,8 @@ Imported release as #15
|
|
|
67
69
|
else {
|
|
68
70
|
const sizeMb = (fileBuffer.length / 1024 / 1024).toFixed(2);
|
|
69
71
|
this.log(`Imported release as #${result.id} (${sizeMb} MB)`);
|
|
72
|
+
for (const line of releasePolicyRunLines(result.policy_run, result.name, result.id))
|
|
73
|
+
this.log(line);
|
|
70
74
|
}
|
|
71
75
|
}
|
|
72
76
|
catch (error) {
|
|
@@ -10,4 +10,11 @@ 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;
|
|
13
20
|
}
|
|
@@ -1,13 +1,21 @@
|
|
|
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';
|
|
4
5
|
export default class ReleaseList extends BaseCommand {
|
|
5
|
-
static description =
|
|
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)";
|
|
6
7
|
static examples = [
|
|
7
8
|
`$ xano release list
|
|
8
9
|
Releases in workspace 5:
|
|
9
10
|
- v1.0 (ID: 10) - main
|
|
10
11
|
- 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>
|
|
11
19
|
`,
|
|
12
20
|
`$ xano release list -w 5 --output json`,
|
|
13
21
|
];
|
|
@@ -57,22 +65,19 @@ Releases in workspace 5:
|
|
|
57
65
|
else {
|
|
58
66
|
this.error('Unexpected API response format');
|
|
59
67
|
}
|
|
68
|
+
const checks = await this.policyChecks(profile, String(workspaceId), releases, flags.verbose);
|
|
60
69
|
if (flags.output === 'json') {
|
|
61
|
-
this.log(JSON.stringify(buildPagingJson({ items: releases }, { tier: 'none' }), null, 2));
|
|
70
|
+
this.log(JSON.stringify(buildPagingJson({ items: withPolicyChecks(releases, checks) }, { tier: 'none' }), null, 2));
|
|
62
71
|
}
|
|
63
72
|
else if (releases.length === 0) {
|
|
64
73
|
this.log('No releases found');
|
|
65
74
|
}
|
|
66
75
|
else {
|
|
67
76
|
this.log(`Releases in workspace ${workspaceId}:`);
|
|
68
|
-
for (const release of releases)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
? ` (${new Date(release.created_at).toLocaleString(undefined, { timeZoneName: 'short' })})`
|
|
73
|
-
: '';
|
|
74
|
-
this.log(` - ${release.name} (ID: ${release.id})${branch}${hotfix}${createdAt}`);
|
|
75
|
-
}
|
|
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>');
|
|
76
81
|
}
|
|
77
82
|
}
|
|
78
83
|
catch (error) {
|
|
@@ -84,4 +89,43 @@ Releases in workspace 5:
|
|
|
84
89
|
}
|
|
85
90
|
}
|
|
86
91
|
}
|
|
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
|
+
});
|
|
87
131
|
}
|
|
@@ -14,10 +14,4 @@ export default class ReleasePull extends BaseCommand {
|
|
|
14
14
|
};
|
|
15
15
|
run(): Promise<void>;
|
|
16
16
|
private resolveReleaseName;
|
|
17
|
-
/**
|
|
18
|
-
* Sanitize a document name for use as a filename.
|
|
19
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
20
|
-
* characters that are unsafe in filenames.
|
|
21
|
-
*/
|
|
22
|
-
private sanitizeFilename;
|
|
23
17
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import * as fs from 'node:fs';
|
|
3
|
-
import * as path from 'node:path';
|
|
4
2
|
import snakeCase from 'lodash.snakecase';
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
5
|
import BaseCommand from '../../../base-command.js';
|
|
6
|
-
import { buildApiGroupFolderResolver, parseDocument } from '../../../utils/document-parser.js';
|
|
6
|
+
import { buildApiGroupFolderResolver, buildChannelServerResolver, parseDocument, resolveDocumentPath, } from '../../../utils/document-parser.js';
|
|
7
7
|
export default class ReleasePull extends BaseCommand {
|
|
8
8
|
static description = 'Pull a release multidoc from the Xano Metadata API and split into individual files';
|
|
9
9
|
static examples = [
|
|
@@ -123,96 +123,19 @@ Pulled 58 documents from release 'v1.0'
|
|
|
123
123
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
124
124
|
// Resolve api_group names to unique folder names, disambiguating collisions
|
|
125
125
|
const getApiGroupFolder = buildApiGroupFolderResolver(documents, snakeCase);
|
|
126
|
+
// Resolve a realtime v2 channel path -> owning realtime_server name, so
|
|
127
|
+
// messages can nest under their channel's server (see resolver docs).
|
|
128
|
+
const getChannelServer = buildChannelServerResolver(documents);
|
|
126
129
|
// Track filenames per type to handle duplicates
|
|
127
130
|
const filenameCounters = new Map();
|
|
128
131
|
let writtenCount = 0;
|
|
129
132
|
for (const doc of documents) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
else if (doc.type === 'workspace_trigger') {
|
|
138
|
-
// workspace_trigger → workspace/trigger/{name}.xs
|
|
139
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
140
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
141
|
-
}
|
|
142
|
-
else if (doc.type === 'error_trigger') {
|
|
143
|
-
// error_trigger → workspace/trigger/{name}.xs (singleton, colocated with workspace triggers)
|
|
144
|
-
typeDir = path.join(outputDir, 'workspace', 'trigger');
|
|
145
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
146
|
-
}
|
|
147
|
-
else if (doc.type === 'agent') {
|
|
148
|
-
// agent → ai/agent/{name}.xs
|
|
149
|
-
typeDir = path.join(outputDir, 'ai', 'agent');
|
|
150
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
151
|
-
}
|
|
152
|
-
else if (doc.type === 'mcp_server') {
|
|
153
|
-
// mcp_server → ai/mcp_server/{name}.xs
|
|
154
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server');
|
|
155
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
156
|
-
}
|
|
157
|
-
else if (doc.type === 'tool') {
|
|
158
|
-
// tool → ai/tool/{name}.xs
|
|
159
|
-
typeDir = path.join(outputDir, 'ai', 'tool');
|
|
160
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
161
|
-
}
|
|
162
|
-
else if (doc.type === 'agent_trigger') {
|
|
163
|
-
// agent_trigger → ai/agent/trigger/{name}.xs
|
|
164
|
-
typeDir = path.join(outputDir, 'ai', 'agent', 'trigger');
|
|
165
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
166
|
-
}
|
|
167
|
-
else if (doc.type === 'mcp_server_trigger') {
|
|
168
|
-
// mcp_server_trigger → ai/mcp_server/trigger/{name}.xs
|
|
169
|
-
typeDir = path.join(outputDir, 'ai', 'mcp_server', 'trigger');
|
|
170
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
171
|
-
}
|
|
172
|
-
else if (doc.type === 'table_trigger') {
|
|
173
|
-
// table_trigger → table/trigger/{name}.xs
|
|
174
|
-
typeDir = path.join(outputDir, 'table', 'trigger');
|
|
175
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
176
|
-
}
|
|
177
|
-
else if (doc.type === 'realtime_channel') {
|
|
178
|
-
// realtime_channel → realtime/channel/{name}.xs
|
|
179
|
-
typeDir = path.join(outputDir, 'realtime', 'channel');
|
|
180
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
181
|
-
}
|
|
182
|
-
else if (doc.type === 'realtime_trigger') {
|
|
183
|
-
// realtime_trigger → realtime/trigger/{name}.xs
|
|
184
|
-
typeDir = path.join(outputDir, 'realtime', 'trigger');
|
|
185
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
186
|
-
}
|
|
187
|
-
else if (doc.type === 'api_group') {
|
|
188
|
-
// api_group "test" → api/{resolved_folder}/{name}.xs
|
|
189
|
-
const groupFolder = getApiGroupFolder(doc.name);
|
|
190
|
-
typeDir = path.join(outputDir, 'api', groupFolder);
|
|
191
|
-
baseName = this.sanitizeFilename(doc.name);
|
|
192
|
-
}
|
|
193
|
-
else if (doc.type === 'query' && doc.apiGroup) {
|
|
194
|
-
// query in group "test" → api/{resolved_folder}/{query_name}.xs
|
|
195
|
-
const groupFolder = getApiGroupFolder(doc.apiGroup);
|
|
196
|
-
const nameParts = doc.name.split('/');
|
|
197
|
-
const leafName = nameParts.pop();
|
|
198
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
199
|
-
typeDir = path.join(outputDir, 'api', groupFolder, ...folderParts);
|
|
200
|
-
baseName = this.sanitizeFilename(leafName);
|
|
201
|
-
if (doc.verb) {
|
|
202
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
else {
|
|
206
|
-
// Default: split folder path from name
|
|
207
|
-
const nameParts = doc.name.split('/');
|
|
208
|
-
const leafName = nameParts.pop();
|
|
209
|
-
const folderParts = nameParts.map((part) => snakeCase(part));
|
|
210
|
-
typeDir = path.join(outputDir, doc.type, ...folderParts);
|
|
211
|
-
baseName = this.sanitizeFilename(leafName);
|
|
212
|
-
if (doc.verb) {
|
|
213
|
-
baseName = `${baseName}_${doc.verb}`;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
133
|
+
const { baseName, typeDir } = resolveDocumentPath(doc, outputDir, {
|
|
134
|
+
getApiGroupFolder,
|
|
135
|
+
getChannelServer,
|
|
136
|
+
join: path.join,
|
|
137
|
+
snakeCase,
|
|
138
|
+
});
|
|
216
139
|
fs.mkdirSync(typeDir, { recursive: true });
|
|
217
140
|
// Track duplicates per directory
|
|
218
141
|
const dirKey = path.relative(outputDir, typeDir);
|
|
@@ -223,8 +146,7 @@ Pulled 58 documents from release 'v1.0'
|
|
|
223
146
|
const count = typeCounters.get(baseName) || 0;
|
|
224
147
|
typeCounters.set(baseName, count + 1);
|
|
225
148
|
// Append numeric suffix for duplicates
|
|
226
|
-
|
|
227
|
-
filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
|
|
149
|
+
const filename = count === 0 ? `${baseName}.xs` : `${baseName}_${count + 1}.xs`;
|
|
228
150
|
const filePath = path.join(typeDir, filename);
|
|
229
151
|
fs.writeFileSync(filePath, doc.content, 'utf8');
|
|
230
152
|
writtenCount++;
|
|
@@ -257,12 +179,4 @@ Pulled 58 documents from release 'v1.0'
|
|
|
257
179
|
}
|
|
258
180
|
return match.id;
|
|
259
181
|
}
|
|
260
|
-
/**
|
|
261
|
-
* Sanitize a document name for use as a filename.
|
|
262
|
-
* Strips quotes, replaces spaces with underscores, and removes
|
|
263
|
-
* characters that are unsafe in filenames.
|
|
264
|
-
*/
|
|
265
|
-
sanitizeFilename(name) {
|
|
266
|
-
return snakeCase(name.replaceAll('"', ''));
|
|
267
|
-
}
|
|
268
182
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
-
import
|
|
3
|
+
import path from 'node:path';
|
|
4
4
|
import BaseCommand from '../../../base-command.js';
|
|
5
|
-
import { findFilesWithGuid } from '../../../utils/document-parser.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';
|
|
6
8
|
export default class ReleasePush extends BaseCommand {
|
|
7
9
|
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.';
|
|
8
10
|
static examples = [
|
|
@@ -72,7 +74,7 @@ Output release details as JSON
|
|
|
72
74
|
};
|
|
73
75
|
async run() {
|
|
74
76
|
const { flags } = await this.parse(ReleasePush);
|
|
75
|
-
const {
|
|
77
|
+
const { profile, profileName } = this.resolveProfile(flags);
|
|
76
78
|
// Determine workspace_id from flag or profile
|
|
77
79
|
let workspaceId;
|
|
78
80
|
if (flags.workspace) {
|
|
@@ -110,6 +112,12 @@ Output release details as JSON
|
|
|
110
112
|
if (documentEntries.length === 0) {
|
|
111
113
|
this.error(`All .xs files in ${flags.directory} are empty`);
|
|
112
114
|
}
|
|
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();
|
|
113
121
|
const multidoc = documentEntries.map((d) => d.content).join('\n---\n');
|
|
114
122
|
// Construct the API URL with query params
|
|
115
123
|
const queryParams = new URLSearchParams({
|
|
@@ -158,6 +166,10 @@ Output release details as JSON
|
|
|
158
166
|
this.error(errorMessage);
|
|
159
167
|
}
|
|
160
168
|
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);
|
|
161
173
|
if (flags.output === 'json') {
|
|
162
174
|
this.log(JSON.stringify(release, null, 2));
|
|
163
175
|
}
|
|
@@ -171,8 +183,14 @@ Output release details as JSON
|
|
|
171
183
|
this.log(` Description: ${release.description}`);
|
|
172
184
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
173
185
|
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);
|
|
174
190
|
this.log(` Time: ${elapsed}s`);
|
|
175
191
|
}
|
|
192
|
+
if (skipped)
|
|
193
|
+
this.warn(skipped);
|
|
176
194
|
}
|
|
177
195
|
catch (error) {
|
|
178
196
|
if (error instanceof Error) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import * as yaml from 'js-yaml';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
|
-
import
|
|
4
|
+
import path from 'node:path';
|
|
5
5
|
import BaseCommand from '../../../../base-command.js';
|
|
6
6
|
export default class SandboxEnvGetAll extends BaseCommand {
|
|
7
7
|
static description = 'Get all environment variables for a sandbox environment and save to a YAML file';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import * as yaml from 'js-yaml';
|
|
3
3
|
import * as fs from 'node:fs';
|
|
4
|
-
import
|
|
4
|
+
import path from 'node:path';
|
|
5
5
|
import BaseCommand from '../../../../base-command.js';
|
|
6
6
|
export default class SandboxEnvSetAll extends BaseCommand {
|
|
7
7
|
static description = '[CRITICAL] STOP and confirm with the user; this replaces all environment variables with the imported file. Sets all environment variables for a sandbox environment from a YAML file.';
|
|
@@ -36,7 +36,7 @@ Sandbox Environment: (tc24-abcd-x1y2)
|
|
|
36
36
|
this.log(` Domain: ${tenant.xano_domain}`);
|
|
37
37
|
if (tenant.sandbox_expires_at) {
|
|
38
38
|
const expiresAt = new Date(tenant.sandbox_expires_at);
|
|
39
|
-
if (!isNaN(expiresAt.getTime())) {
|
|
39
|
+
if (!Number.isNaN(expiresAt.getTime())) {
|
|
40
40
|
const msLeft = expiresAt.getTime() - Date.now();
|
|
41
41
|
if (msLeft > 0) {
|
|
42
42
|
const minsLeft = Math.ceil(msLeft / 60_000);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
2
|
import * as fs from 'node:fs';
|
|
3
|
-
import
|
|
3
|
+
import path from 'node:path';
|
|
4
4
|
import BaseCommand from '../../../../base-command.js';
|
|
5
5
|
export default class SandboxLicenseGet extends BaseCommand {
|
|
6
6
|
static description = 'Get the license for a sandbox environment';
|